一次前端资源 Gzip 压缩失效的深度排查与反思

一个看似简单的 Gzip 压缩问题,背后却牵扯出 K8s Ingress、多层 Nginx 代理、请求头传递等一系列链路问题。本文将完整复盘一次从现象到根因的深度排查过程,并对不同解决方案进行对比和反思。

error_outline一、问题的提出:Gzip 压缩为何失效?

我们发现线上环境的前端静态资源 https://eu.ampaura.tech/ems/umi.55c10ef3.js 未开启 Gzip 压缩,导致文件体积过大,影响了页面加载性能。然而,检查业务容器内的 Nginx 配置后发现,Gzip 功能明明是开启的。这说明压缩在中间链路的某个环节被“熔断”了。

search二、排查之旅:层层递进的证据链

Step 1: 外部验证

首先,从外部直接请求资源,确认浏览器收到的响应头中确实**不包含** Content-Encoding: gzip

curl -sI https://eu.ampaura.tech/ems/umi.55c10ef3.js
# ...
# 响应头中未找到 Content-Encoding
# ...

Step 2: 深入业务容器 (ems-front)

进入最终提供服务的 Pod (ems-front-xxxx),直接在容器内部请求 Nginx,模拟最纯粹的环境。

kubectl exec -n ems-eu ems-front-6cf688c88-vkjlb -- \
  curl -sI -H 'Accept-Encoding: gzip' http://127.0.0.1/umi.55c10ef3.js

结果令人惊喜:响应头中明确包含了 Content-Encoding: gzip!这证明了**业务容器自身的 Nginx 压缩是正常的**。问题一定出在上游的代理链路上。

Step 3: 检查静态文件

一个常见的优化是“静态预压缩”,即在构建阶段就生成 .gz 文件。我们检查了容器内是否存在这些文件。

kubectl exec -n ems-eu ems-front-6cf688c88-vkjlb -- find /app/dist -name '*.gz'
# (无输出)

结果为空,排除了使用 gzip_static 模块的可能性。压缩是在 Nginx 运行时动态进行的。

Step 4: 定位中间链路 (ems-common-front)

通过检查 K8s 的 Ingress 和 Service 配置,我们发现外部流量并不是直接打到 ems-front,而是先经过了一个名为 ems-common-front 的**聚合层 Nginx**。

现在,我们在聚合层 Pod 内部,去请求下游的 ems-front 服务。

kubectl exec -n ems-eu ems-common-front-d86f65654-2s8k6 -- \
  curl -sI -H 'Accept-Encoding: gzip' http://ems-front/umi.55c10ef3.js

结果再次确认,下游服务 (ems-front) 返回了 Gzip 压缩内容。到此,真相水落石出:问题就出在 ems-common-front 这个聚合层 Nginx 上。

lightbulb三、根因分析:被“遗忘”的请求头

通过检查 ems-common-front 的 Nginx 配置 (存储在 ConfigMap 中),我们发现了两个相互关联的缺陷:

  1. 未向上游转发 Accept-Encoding 头: Nginx 作为反向代理,默认不会将所有客户端请求头都转发给上游。由于聚合层没有显式转发 Accept-Encoding,导致下游的 ems-front 认为客户端不支持 Gzip,因此返回了未压缩的原始内容。
  2. 自身 gzip_types 配置不完整: 聚合层 Nginx 自身虽然开启了 gzip on;,但其 gzip_types 配置列表很老旧,只包含了 application/x-javascript,而 Umi 打包后的 JS 文件 MIME 类型是 application/javascript。因此,即使下游返回了明文,聚合层也未能对其进行二次压缩。

construction四、解决方案对比与推荐

方案一:转发请求头 (推荐)

在聚合层 Nginx 的 `location` 块中,添加 proxy_set_header Accept-Encoding $http_accept_encoding;,让下游的压缩能力得到充分利用。

  • 优点: 改动最小,不增加聚合层CPU开销。
  • 缺点: 依赖下游压缩。

方案二:聚合层压缩

在聚合层 Nginx 中,将 gzip_types 补全,至少加入 application/javascriptapplication/json

  • 优点: 即使下游不压缩,出口也能保证压缩。
  • 缺点: 增加聚合层CPU开销,内网带宽浪费。

方案三:静态预压缩

在前端构建流程中生成 .gz/.br 文件,并在两层 Nginx 中都开启 gzip_static on;

  • 优点: 性能最优,最省CPU。
  • 缺点: 增加 CI/CD 复杂度。

推荐实现

我们采取了**“双管齐下”**的策略,以兼顾性能和兜底能力:

  1. **首选方案一:** 在 ems-common-front 的 ConfigMap 中添加 proxy_set_header Accept-Encoding $http_accept_encoding;,让请求头得以透传。
  2. **同时执行方案二:** 扩充 gzip_types,确保即使下游偶尔返回明文,聚合层也能进行兜底压缩。

forum五、追问与反思

help_outline为何没有先怀疑 Ingress 呢?

这是一个很好的问题。在本次排查中,Ingress 的嫌疑被后置,主要基于以下判断:

  • 默认行为: Nginx Ingress Controller 默认仅做透传,不会主动解压或修改压缩相关的响应头,除非有特定的注解 (Annotation)。我们检查了 Ingress 对象,未发现此类注解。
  • 链路特征: 外部请求响应头中虽然没有 Content-Encoding,但包含了 Vary: Accept-Encoding,这暗示着链路中至少有服务考虑了编码问题。如果是 Ingress 强制解压,这些头信息可能也会被清除。
  • 交叉验证: 在聚合层 Pod 内部直接访问下游服务(有效)和公网域名(无效)的对比测试,是**定位问题的关键**。它将问题范围精确地锁定在了“聚合层 Nginx 的出口”这一环节。
lightbulb为何只改 gzip_types 也能生效?

确实,只在聚合层补全 gzip_types 也能让浏览器收到压缩内容。但这其实是一个“将错就错”的结果:

  1. 聚合层 Nginx (ems-common-front) 未向上游转发 Accept-Encoding
  2. 下游 Nginx (ems-front) 收到一个“不接受压缩”的请求,因此返回了**未压缩**的 JS 文件(约 940KB)。
  3. 聚合层 Nginx 收到这个明文响应,此时因为我们补全了 gzip_types,它识别出 application/javascript 是需要压缩的类型,于是**自己动手**进行了压缩,并返回给浏览器。

这种方式虽然能“解决”问题,但会在内网中传输更大的未压缩文件,并增加聚合层的 CPU 负载。因此,透传 Accept-Encoding 仍然是更优的实践。